package parsetree;

import java.awt.Color;
import java.util.LinkedList;
import java.util.List;

import javax.swing.event.DocumentEvent.ElementChange;
import javax.swing.text.AbstractDocument.ElementEdit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import metagrammar.Type;
import parsetree.exceptions.DocumentContractBrokenException;
import util.TrieSet;

public class ParsingElement extends AbstractLeafElement {
    
    public static final String NAME = "parsing";
    
    private static final AttributeSet DEFAULT_STYLE;
    static {
        SimpleAttributeSet attributes = new SimpleAttributeSet();
        attributes.addAttribute(StyleConstants.Foreground, Color.GRAY);
        DEFAULT_STYLE = attributes;
    }
    
    private final Position endPosition;
    
    public ParsingElement(BranchElement parent, int startOffset, int endOffset)
            throws BadLocationException
            {
        super(parent, startOffset);
        endPosition = getDocument().createPosition(endOffset);
        int offset = startOffset;
        int length = endOffset - startOffset;
        getDocument().setCharacterAttributes(offset, length, DEFAULT_STYLE, true);
            }
    
    private ParsingElement(BranchElement parent, Position startPosition, Position endPosition)
    {
        super(parent, startPosition);
        this.endPosition = endPosition;
        getDocument().setCharacterAttributes(getStartOffset(), getStartOffset() + getEndOffset(),
                DEFAULT_STYLE, true);
    }
    
    private ParsingElement(BranchElement parent, int startOffset, Position endPosition)
            throws BadLocationException
            {
        super(parent, startOffset);
        this.endPosition = endPosition;
        int offset = startOffset;
        int length = getEndOffset() - startOffset;
        getDocument().setCharacterAttributes(offset, length, DEFAULT_STYLE, true);
            }
    
    @Override
    public Type getType()
    {
        return Type.UNTYPED;
    }
    
    @Override
    public String getName()
    {
        return NAME;
    }
    
    private List<ElementChange> parseString(Insert change)
    {
        /*if(change != null && change.getOffset() < getStartOffset()) {
            int index = getParent().getIndexOfChildContaining(change.getOffset());
            if(index < 0 && -(index+1) ) {
                
            }
            change.addEdit(new ElementEdit(this, this.getStartOffset(), null,
                    new Element[] { this }));
            ParsingElement parser;
            try {
                parser = new ParsingElement(getParent(), change.getOffset(), getEndPosition());
                getParent().addChild(parser);
                return parser.insertUpdate(change, DEFAULT_STYLE);
            }
            catch(BadLocationException e) {
                throw new DocumentContractBrokenException("Parsing has become impossible.");
            }
        }*/
        LinkedList<TrieSet<ElementFactory>.TrieSearchResult> previousResults = new LinkedList<TrieSet<ElementFactory>.TrieSearchResult>();
        TrieSet<ElementFactory>.TrieSearchResult currentResult = getParseTree().getPrefixMap()
                .findSuffixTree("");
        int length = getStartOffset();
        StyledDocument document = getDocument();
        LinkedList<ElementChange> elementChanges = new LinkedList<ElementChange>();
        try {
            for(; currentResult.finishedSearchingKey() && length < getEndOffset(); length++) {
                previousResults.push(currentResult);
                currentResult = currentResult.appendToSearch(document.getText(length, 1));
            }
            if( !currentResult.finishedSearchingKey()) {
                // if there is only one possible parsing solution before the end of the text is reached
                TrieSet<ElementFactory>.TrieSearchResult map;
                do { // find the last valid parse
                    map = previousResults.pop();
                } while( !previousResults.isEmpty()
                        && ( !map.finishedSearchingSuffix() || map.getNode().getData() == null));
                if( !previousResults.isEmpty()
                        || (map.finishedSearchingSuffix() && map.getNode().getData() != null)) {
                    // if a valid parse is found
                    // recover the operator
                    ElementFactory spec = map.getNode().getData();
                    // create a new operator element representing the operator
                    int start = getStartOffset();
                    int operatorLength = map.getKeySearchLength();
                    BranchElement parent = getParent();
                    Element element = spec.generateElement(parent, start, start
                            + operatorLength);
                    elementChanges.add(parent.addChild(element));
                }
                else {
                    elementChanges.add(new ElementEdit(this, this.getStartOffset(), null, null));
                }
            }
            else if(currentResult.getNode().isLeaf() && currentResult.finishedSearchingSuffix()) {
                // the end of the text has been reached.
                // if there is only one possible parsing solution...
                TrieSet<ElementFactory>.TrieSearchResult map = currentResult;
                while( !previousResults.isEmpty()
                        && (map.finishedSearchingSuffix() && map.getNode().getData() == null)) {
                    map = previousResults.pop();
                } // find the last valid solution
                if( !previousResults.isEmpty()
                        || ( !map.finishedSearchingSuffix() && map.getNode().getData() != null)) {
                    ElementFactory operatorSpec = map.getNode().getData();
                    BranchElement parent = getParent();
                    Element element = operatorSpec.generateElement(parent,
                            getStartOffset(), getEndOffset());
                    elementChanges.add(parent.addChild(element));
                }
                else {
                    elementChanges.add(new ElementEdit(this, this.getStartOffset(), null, null));
                }
            }
        }
        catch(BadLocationException e1) {
            throw new DocumentContractBrokenException("Parsing has become impossible.");
        }
        return elementChanges;
    }
    
    @Override
    public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
    {
        if(change.getOffset() < getStartOffset()
                && change.getOffset() + change.getLength() <= getStartOffset()) {
            return null;
        }
        return parseString(change);
    }
    
    @Override
    public List<ElementChange> postRemoveUpdate(Removal change)
    {
        if(getStartOffset() == getEndOffset()) {
            ElementEdit removal = getParent().removeEmptyChild(this);
            LinkedList<ElementChange> changes = new LinkedList<ElementChange>();
            changes.add(removal);
            return changes;
        }
        return null;
    }
    
    @Override
    public Element merge(Element element)
    {
        if(element instanceof ParsingElement && getParent().equals(element.getParent())) {
            if(getEndOffset() == element.getStartOffset()) {
                return handleMerge(this, (ParsingElement) element);
            }
            else if(getStartOffset() == element.getEndOffset()) {
                return handleMerge((ParsingElement) element, this);
            }
        }
        return null;
    }
    
    /**
     * Handels the actual merge between two adjacent parse elements.
     * @param firstElement
     * @param element
     * @return
     */
    private Element handleMerge(ParsingElement firstElement, ParsingElement secondElement)
    {
        BranchElement parent = firstElement.getParent();
        return new ParsingElement(parent, firstElement.getStartPosition(),
                secondElement.getEndPosition());
    }
    
    private Position getEndPosition()
    {
        return endPosition;
    }
    
    @Override
    public int getEndOffset()
    {
        return endPosition.getOffset();
    }
    
}